我第一次接觸到 RAG 是在 Hello World Dev Conference 的 workshop 中。雖然 workshop 只是簡單地帶我們實作從一個 PDF 檔案中搜尋內容,但這次體驗讓我了解到目前市場上常見解決方案背後的原理。為了進一步了解 RAG 的運作方式,我決定自己實作一個小小的 side project 試試看。
RAG 是什麼?
RAG,即 Retrieval-Augmented Generation,是一種結合檢索機制與生成模型的 AI 技術。傳統語言模型雖然功能強大,但其內部知識庫是固定的,可能會隨時間變得過時或不完整。RAG 透過兩個步驟解決這個問題:
- 檢索:從外部知識來源(例如資料庫或文件庫)中找到相關且最新的資訊。
- 生成:利用檢索到的資訊生成答案,確保回答準確且與當前情境相關。
這種檢索與生成的結合,使 AI 系統能產出更具時效性的答案,非常適合需要最新資訊的應用場景,例如客服支援、新聞更新或學術研究。目前,檢索步驟通常採用向量搜尋(vector search)技術來尋找相關資料。
向量搜尋是什麼?
向量資料庫使用向量表示每個物件,以保留資料的語義關係。將語意轉換成向量的過程稱為 embedding,而每個向量對應多維空間中的一個點。例如,在下圖中,香蕉的向量位置會比較靠近蘋果,而雞跟貓的向量位置會比較相近:
至於這些向量所在的多維空間到底有多少維度,目前最普遍被使用的 OpenAI text-embedding-3-small
模型提供 1536 維度,而進階的 text-embedding-3-large 模型則提供 3072 維度。這些高維度特性,能帶來更精確的結果。
Steps
RAG 通常由以下的步驟構成:
- 載入:使用工具(如 langchain 的 Document loader)載入資料。
- 分割:使用 Text Splitter 將大型文件分成小片段,以利於索引及傳入模型處理,因為大型片段難以搜尋且不適合模型的 context window。
- 儲存:需要一個儲存和索引這些片段的地方,以便日後進行搜尋。這通常使用向量資料庫和 embedding 模型來完成。
- 檢索:根據使用者輸入,使用 Retriever 從儲存中獲取相關片段。
- 生成:LLM 利用問題與檢索結果生成答案。
在這次的 RAG demo 中,我使用 我的部落格 的文章作為資料來源,並採用 Rails 作為網頁框架,向量資料庫則使用 Qdrant 提供的免費雲端方案。
Ruby Code
載入
由於資料來源是 markdown 檔案,載入部分直接使用 Ruby 的 File.read(file_path)
方法。若使用更成熟的框架(如 Langchain),則可支援載入多種來源(如 PDF 或網頁)。
分割
一般來說,分割會將檔案切成多個小片段並儲存到向量資料庫,同時需維護檔案與資料庫間的關聯,方便日後更新。而為了簡化這個 side project,我只有在文章超過 token 限制時,將文章分段後使用 LLM 進行摘要,再將摘要儲存到向量資料庫中,如此一來我就不用使用另一個資料庫儲存相對應的關聯資訊。雖然這可能導致部分細節遺失,但對於這次 demo 已經足夠。
我使用的是 langchainrb
套件中的 Chunker::RecursiveText
來進行分段。順帶一提雖然 langchainrb
套件目前功能比 Python 的 Langchain 提供的功能陽春很多,但已能滿足我的大部分需求。
1 | class AITextSummarizer |
儲存
儲存資料需要嵌入模型(embedding model)將資料轉換為向量,然後將結果存入向量資料庫。在我的實作中,我將文章內容、標題及網址等相關資訊一起存入向量資料庫的 payload(可以想像成 metadata),這樣最後 LLM 回答問題時就可以直接告訴我相關的網址跟文章標題。
閱讀下面的程式碼前有幾點需要先知道
langchainrb
套件的Langchain::Vectorsearch::Qdrant
會自動幫忙使用 llm 的 embedding model 轉成向量,所以只要提供使用的 llm API key 即可- Qdrant 的專有名詞
a. collection 是用來儲存 points 的集合,可以想像成是 RDBMS 裡面的 table
b. point 是在向量資料庫的最重要 entity,一個點包含了一個向量跟 payload,可以想像成是 RDBMS 裡面的 row - 使用之後才發現 Qdrant 中 point 的 id 必須遵守幾種可能的格式(ref),我這裡使用文章的發布日期轉成 uuid 作為 point 的 id,如此一來同一篇文章更新的時候,同樣的發布日期就可以直接取代舊的 point
1 | add_point(file_name: file_name, original_content: content, extracted_content: extracted_content) |
檢索 & 生成
langchainrb
套件的 Langchain::Vectorsearch::Qdrant
模組會自動使用 LLM 的 embedding model 把問題轉成向量並進行相似度搜尋。搜尋結果返回後,LLM 將根據問題和檢索資料生成答案。
以下程式碼中的 k 表示從相似度搜尋中返回的 point 數量,LLM 會以這些 point 的資料作為生成答案的基礎。
1 | class QuestionsController < ApplicationController |
Demo
以上就是這個小專案中跟 RAG 有關的部分。
在 UI 實作方面,,我使用 Rails 搭配 Turbo 來作出簡單的問答頁面,成果長得像下面這樣:
問問題的頁面:
LLM 回答的結果:
從上面的圖片可以看到因為我有給他文章網址,因此他在回答的時候也能提供這部分的資訊。
心得
實作這個小專案的過程中,我才真正體會到開發一個 RAG 應用程式時需要注意的各種細節。例如,當一篇文章被更新時,如何有效地同步更新向量資料庫中的相關資料,以及針對長篇文章,有哪些分割方法可以使用,而哪些方法的效果大家實驗後覺得較為理想,這些都是值得研究的議題。
接下來,我計劃進一步學習 LangChain 的使用方式,透過實作幾個小應用來熟悉他。接著希望能深入了解實作細節和所使用的 prompt 等等,可能未來能更靈活地應用到其他專案中。